냄새와 휴리스틱

아래 소개하는 목록에서 마틴 파울러가 코드에서 맡은 냄새와, 저자가 코드를 짜면서 사용하는 기교와 휴리스틱1을 확인할 수 있다.

주석

부적절한 정보

주석은 코드와 설계에 기술적인 설명을 부연하는 수단이다. 장황하고 따분한 내용으로 소스 코드만 번잡하게 만들지 말고 작성자, SPRSoftware Problem Report번호 등과 같은 메타 정보만 주석으로 넣는다.

쓸모 없는 주석

주석은 빨리 낡는다. 쓸모 없어질 주석은 아예 달지 말고, 쓸모 없어진 주석은 재빨리 삭제하자.

중복된 주석

주석은 코드만으로 다하지 못하는 설명을 부언하는 것이다. 코드만으로 충분한데 구구절절 설명하지 말자.

/**
 * @param {string} [name] - Somebody's name.
 */
function sayHello(name = "John Doe") {
    alert(`Hello ${name}`);
}

성의 없는 주석

작성할 가치가 있는 주석을 작성할 때는 간결하고 명료하게 작성하자.
단어를 신중히 선택하고, 문법과 구두점을 올바로 사용하며, 주절대거나 당연한 소리를 반복하지 말자.

주석 처리된 코드

코드를 주석으로 처리하지 말고, 혹 발견하거든 즉시 삭제하자.

주석 처리된 코드는 얼마나 오래된 코드인지, 중요한 코드인지 아닌지, 알 길이 없다.
코드는 그 자리에서 낡아가지만, 혹여 누군가 필요할까 아무도 삭제하지 않는다.
버전 관리 시스템을 믿고 사용하지 않는 코드는 삭제하자.

환경

여러 단계로 빌드해야 한다

빌드는 한 명령으로 전체를 체크아웃해서 한 명령으로 간단히 할 수 있어야 한다.

npm run build
npm run deploy

여러 단계로 테스트해야 한다

모든 단위 테스트는 한 명령으로 돌려야 한다.
IDE에서 버튼 하나로 모든 테스트가 돌아가는 게 가장 이상적이나, 아무리 열악한 환경이라도 셸에서 명령 하나로 가능해야 한다.

함수

너무 많은 인수

함수의 인수는 그 개수가 작을수록 좋다.

출력 인수

일반적으로 독자는 인수를 출력이 아닌 입력으로 간주하기에, 출력 인수는 직관을 정면으로 위배한다.

플래그 인수

boolean 인수는 함수가 여러 기능을 수행한다는 명백한 증거다. 플래그 인수는 혼란을 초래하므로 피해야 마땅하다.

죽은 함수

아무도 호출하지 않는 함수는 삭제한다.
상술한 것과 마찬가지로 버전 관리 시스템을 믿고 과감히 삭제하라.

일반

한 소스 파일에 여러 언어를 사용한다

이상적으로는 소스 파일 하나에 언어 하나만 사용하는 방식이 가장 좋다.
불가피할 때도 각별한 노력으로 소스 파일에서 언어 수와 범위를 줄이려 애쓸 필요가 있다.

당연한 동작을 구현하지 않는다

최소 놀람의 원칙The Principle of Least Surprise2에 의거해 함수나 클래스는 다른 프로그래머가 당연하게 여길 만한 동작과 기능을 제공해야 한다.
당연한 동작을 구현하지 않으면 코드를 직관적으로 읽는 것이 어려워지고, 저자를 신뢰하지 못하게 돼 코드를 일일이 살펴야 한다.

경계를 올바로 처리하지 않는다

코드는 올바로 동작해야 한다. 하지만 올바른 동작은 보통 아주 복잡하다.
모든 경계와 구석진 곳에서 코드를 증명하고, 직관에만 의존하지 마라.

안전 절차 무시

컴파일러 경고 등을 꺼버리면 개발이나 빌드가 쉬워질지 모르지만 자칫하면 끝없는 디버깅에 시달리게 된다. 실패하는 테스트 케이스를 나중으로 미루지 말자.
체르노빌Chernobyl 원전 사고가 수행하기 번거로워 안전 절차를 무시한 책임자에게서 유발됐음을 명심하자.

중복

가장 중요한 규칙 중 하나이다.

코드에서 중복을 발견할 때마다 추상화할 기회로 간주하라. 중복된 코드를 하위 루틴이나 다른 클래스로 분리하면 설계 언어의 어휘가 늘어나며 다른 프로그래머들이 어휘를 사용하기 쉬워진다. 또한 구현이 빨라지고 오류도 적어진다.

가장 뻔한 유형은 똑같은 코드가 여러 차례 나오는 중복이다. 간단한 함수로 교체한다.

좀 더 미묘한 유형은 여러 모듈에서 일련의 switch/case나 if/else 문으로 똑같은 조건을 거듭 확인하는 중복이다. 이런 중복은 다형성polymorphism으로 대체한다.

더더욱 미묘한 유형은 알고리즘이 유사하나 코드가 서로 다른 중복이다. TEMPLATE METHOD 패턴3이나 STRATEGY 패턴4으로 중복을 제거한다.

어디서든 중복을 발견하면 없애라.

추상화 수준이 올바르지 못하다

추상화로 개념을 분리할 때는 철저히 저차원 상세 개념에서 고차원 일반 개념을 분리해야 한다.
모든 저차원 개념은 파생 클래스에 넣고, 모든 고차원 개념은 기초 클래스에 넣는다.

잘못된 추상화 수준은 거짓말이나 꼼수로 해결하지 못한다. 추상화는 소프트웨어 개발자에게 가장 어려운 작업 중 하나다. 잘못된 추상화를 임시변통으로 고치기는 불가능하다.

기초 클래스가 파생 클래스에 의존한다

기초 클래스와 파생 클래스로 개념을 나누는 가장 흔한 이유는 고차원 기초 클래스 개념을 저차원 파생 클래스 개념으로부터 분리해 독립성을 보장하기 위해서다. 그러므로 기초 클래스가 파생 클래스를 사용한다면 뭔가 문제가 있다는 말이다. 일반적으로 기초 클래스는 파생 클래스를 아예 몰라야 마땅하다.

과도한 정보

잘 정의된 모듈은 아주 작은 인터페이스로도 많은 동작이 가능하다. 잘 정의된 인터페이스는 많은 함수를 제공하지 않아 결합도coupling가 낮다.

클래스가 제공하는 메서드 수는 적을수록 좋다. 함수가 아는 변수 수도 적을수록 좋다. 클래스에 들어있는 인스턴스 변수 수도 적을수록 좋다.

자료, 유틸리티 함수, 상수, 임시 변수를 숨기고메서드나 인스턴스 변수가 넘쳐나는 클래스는 피하라. 인터페이스를 매우 작게 그리고 매우 깐깐하게 만들어라. 정보를 제한해 결합도를 낮춰라.

죽은 코드

죽은 코드란 불가능한 조건을 확인하는 if 문이나, throw 문이 없는 try 문에서 catch 블록처럼 실행되지 않는 코드를 말한다.

죽은 코드를 발견하면 시스템에서 제거하라.

수직 분리

변수와 함수는 사용되는 위치에 가깝게 정의한다. 지역 변수는 처음으로 사용하기 직전에 선언하며 수직으로 가까운 곳에 있어야 한다.
비공개 함수는 처음으로 호출한 직후에 정의한다. 비공개 함수는 전체 클래스 범위scoped에 속하지만 그래도 정의하는 위치와 호출하는 위치를 가깝게 유지한다.

일관성 부족

어떤 개념을 특정 방식으로 구현했다면 유사한 개념도 같은 방식으로 구현한다.

잡동사니

비어 있는 기본 생성자, 아무도 사용하지 않는 변수, 아무도 호출하지 않는 함수, 정보를 제공하지 못하는 주석 등은 쓸데없이 코드만 복잡하게 만든다. 이는 제거해야 마땅하다.

인위적 결합

서로 무관한 개념을 인위적으로 결합하지 않는다.
함수, 상수, 변수를 선언할 때는 시간을 들여 올바른 위치를 고민한다. 그저 당장 편한 곳에 선언하고 내버려두면 안 된다.

기능 욕심

클래스 메서드는 자기 클래스의 변수와 함수에 관심을 둬야지 다른 클래스의 변수와 함수에 관심을 둬서는 안된다. 메서드가 다른 객체의 참조자accessor와 변경자mutator를 사용해 그 객체 내용을 조작한다면 메서드가 그 객체 클래스의 범위를 욕심내는 탓이다.

선택자 인수

선택자selector 인수는 목적을 기억하기 어려울 뿐 아니라 각 선택자 인수가 여러 함수를 하나로 조합한다. 선택자 인수는 큰 함수를 작은 함수 여럿으로 쪼개지 않으려는 게으름의 소산이다.

모호한 의도

코드를 짤 때는 의도를 최대한 분명히 밝힌다. 행을 바꾸지 않고 표현한 수식, 헝가리식 표기법, 매직 넘버 등은 모두 저자의 의도를 흐린다.

잘못 지운 책임

소프트웨어 개발자가 내리는 가장 중요한 결정 중 하나가 코드를 배치하는 위치다. 코드는 독자가 자연스럽게 기대할 위치에 배치한다.

부적절한 static 함수

Math.max처럼 메서드를 소유하는 객체에서 가져오는 정보가 없고, 재정의override할 가능성이 없을 때가 아니라면 함수를 static으로 정의하지 않는다.

서술적 변수

프로그램 가독성을 높이는 가장 효과적인 방법 중 하나가 계산을 여러 단계로 나누고 중간값으로 서술적인 변수 이름을 사용하는 방법이다.
계산을 몇 단계로 나누고 중간값에 좋은 변수 이름만 붙여도 해독하기 어렵던 모듈이 순식간에 읽기 쉬운 모듈로 탈바꿈한다.

이름과 기능이 일치하는 함수

이름만으로 분명하지 않기에 구현을 살피거나 문서를 뒤적여야 한다면 더 좋은 이름으로 바꾸거나 더 좋은 이름을 붙이기 쉽도록 기능을 정리해야 한다.

알고리즘을 이해하라

구현이 끝났다고 선언하기 전에 함수가 돌아가는 방식을 확실히 이해했는지 확인하라. 테스트 케이스를 모두 통과한다는 사실만으로 부족하다. 작성자가 알고리즘이 올바르다는 사실을 알아야 한다.5
알고리즘이 올바르다는 사실을 확인하고 이해하려면 기능이 뻔히 보일 정도로 함수를 깔끔하고 명확하게 재구성하는 방법이 최고다.

논리적 의존성은 물리적으로 드러내라

한 모듈이 다른 모듈에 의존한다면 물리적인 의존성도 있어야 한다. 의존하는 모든 정보를 명시적으로 요청하는 편이 좋다.

If/Else 혹은 Switch/Case 문보다 다형성을 사용하라

3장6에서 새 유형을 추가할 확률보다 새 함수를 추가할 확률이 높은 코드에선 switch 문이 더 적합하다 주장했다.

대다수 개발자가 switch 문을 사용하는 이유는 당장 손쉬운 선택이기 때문이다. 그러므로 switch를 선택하기 전에 다형성을 먼저 고려해야 한다.
또한, 유형보다 함수가 더 쉽게 변하는 경우는 극히 드물다. 그러므로 모든 switch 문을 의심해야 한다.

표준 표기법을 따르라

팀은 업계 표준에 기반을 둔 구현 표준을 따라야 한다. 구현 표준은 인스턴스 변수 이름을 선언하는 위치, 클래스/메서드/변수 이름을 정하는 방법, 괄호를 넣는 위치 등을 명시해야 한다. 표준을 설명하는 문서는 코드 자체로 충분해야 하며 별도 문서를 만들 필요는 없어야 한다.

팀이 정한 표준은 팀원들 모두가 따라야 한다. 실제 괄호를 넣는 위치보단 모두가 동의한 위치에 넣는다는 사실이 중요하다. 이 사실을 이해할 정도로 팀원들이 성숙해야 한다.

매직 넘버는 명명된 상수로 교체하라

일반적으로 코드에서 숫자를 사용하지 말고, 명명된 상수 뒤로 숨긴다.
'매직 넘버'는 단순히 숫자만 의미하지 않고, 의미가 분명하지 않은 토큰을 모두 가리킨다.

정확하라

코드에서 뭔가를 결정할 때는 정확히 결정한다. 결정을 내리는 이유와 예외를 처리할 방법을 분명히 알아야 한다.
코드에서 모호성과 부정확은 견해차나 게으름의 결과다. 어느 쪽이든 제거해야 마땅하다.

관례보다 구조를 사용하라

설계 결정을 강제할 때는 규칙보다 관례를 사용한다. 명명 관례도 좋지만 구조 자체로 강제하면 더 좋다.

조건을 캡슐화하라

부울 논리는 이해하기 어렵다. 조건의 의도를 분명히 밝히는 함수로 표현하라.

if (shouldBeDeleted(timer))

라는 코드가

if (timer.hasExpired() && !timer.isRecurrent())

라는 코드보다 좋다.

부정 조건은 피하라

부정 조건은 긍정 조건보다 이해하기 어렵다. 가능하면 긍정 조건으로 표현한다.

함수는 한 가지만 해야 한다

한 함수 안에 여러 단락을 이어, 일련의 작업을 수행하는 중이라면 한 가지만 수행하는 좀 더 작은 함수 여럿으로 나눠야 마땅하다.

숨겨진 시간적인 결합

때로는 시간적인 결합이 필요하다. 하지만 시간적인 결합을 숨겨서는 안 된다. 함수를 짤 때는 함수 인수를 적절히 배치해 함수가 호출되는 순서를 명백히 드러낸다.

일관성을 유지하라

코드 구조를 잡을 떄는 이유를 고민하라. 그리고 그 이유를 코드 구조로 명백히 표현하라.

경계 조건을 캡슐화하라

경계 조건은 빼먹거나 놓치기 십상이다. 경계 조건은 여기저기에서 처리하지 않고 한 곳에서 별도로 처리하라.

함수는 추상화 수준을 한 단계만 내려가야 한다

함수 내 모든 문장은 추상화 수준이 같아야 한다. 그리고 그 추상화 수준은 함수 이름이 의미하는 작업보다 한 단계만 낮아야 한다.
추상화 수준 분리는 리팩터링을 수행하는 가장 중요한 이유 중 하나인 동시에, 제대로 하기에 가장 어려운 작업 중 하나이기도 하다.

설정 정보는 최상위 단계에 둬라

추상화 최상위 단계에 둬야 할 기본값 상수나 설정 관련 상수를 저차원 함수에 숨겨서는 안 된다. 대신 고차원 함수에서 저차원 함수를 호출할 때 인수로 넘긴다.

추이적 탐색을 피하라

일반적으로 한 모듈은 주변 모듈을 모를수록 좋다.
이를 디미터의 법칙Law of Demeter7이라 부른다. 자신이 직접 사용하는 모듈만 알아야 하며, 그 모듈이 연이어 자신이 아는 모듈을 따라가며 시스템 전체를 휘젓지 않도록 한다.

이름

서술적인 이름을 사용하라

이름은 성급하게 정하지 않는다. 소프트웨어 가독성의 90%는 이름이 결정한다. 시간을 들여 현명한 이름을 선택하고 유효한 상태로 유지한다.

적절한 추상화 수준에서 이름을 선택해라

구현을 드러내는 이름은 피하고, 작업 대상 클래스나 함수가 위치하는 추상화 수준을 반영하는 이름을 선택하라.

가능하다면 표준 명명법을 사용하라

기존 명명법을 사용하는 이름은 이해하기 더 쉽다.
프로젝트에 유효한 의미가 담긴 이름을 많이 사용할수록 독자가 코드를 이해하기 쉬워진다.

명확한 이름

함수나 변수의 목적을 명확히 밝히는 이름을 선택한다.

긴 범위는 긴 이름을 사용하라

이름 길이는 범위 길이에 비례해야 한다.

function removeOdd<T>(array: T[]): T[] {
    return array.filter((_, i) => i % 2);
}

위 코드에서 iindexOfItem이라고 썼다면 헷갈릴 터이다. 반면, 이름이 짧은 변수나 함수는 범위가 길어지면 의미를 잃는다. 그러므로 이름 범위가 길수록 이름을 정확하고 길게 짓는다.

인코딩을 피하라

이름에 유형 정보나 범위 정보를 넣어서는 안 된다. 오늘날 환경은 이름을 조작하지 않고도 모든 정보를 제공한다. 헝가리안 표기법의 오염에서 이름을 보호하라.

이름으로 부수 효과를 설명하라

함수, 변수, 클래스가 하는 일을 부수 효과를 포함해 모두 기술하는 이름을 사용하라.

테스트

불충분한 테스트

테스트 케이스는 잠재적으로 깨질 만한 부분을 모두 테스트해야 한다. 테스트 케이스가 확인하지 않는 조건이나 검증하지 않는 계산이 있다면 그 테스트는 불완전하다.

커버리지 도구를 사용하라!

커버리지 도구는 테스트가 빠뜨리는 공백을 알려줘 테스트가 불충분한 모듈, 클래스, 함수를 찾기 쉽게 만들어준다.

사소한 테스트를 건너뛰지 마라

사소한 테스트가 제공하는 문서적 가치는 구현에 드는 비용을 넘어선다.

무시한 테스트는 모호함을 뜻한다

불분명한 요구사항은 테스트 케이스를 주석으로 처리하는 등의 방식으로 표현한다. 선택 기준은 모호함이 존재하는 테스트 케이스가 컴파일이 가능한지 불가능한지에 달려있다.

경계 조건을 테스트하라

경계 조건은 특히 신경 써서 테스트한다. 알고리즘의 중앙 조건은 올바로 짜놓고 경계 조건에서 실수하는 경우가 흔하다.

버그 주변은 철저히 테스트하라

버그는 서로 모이는 경향이 있다. 한 함수에서 버그를 발견했다면 그 함수를 철저히 테스트하는 편이 좋다.

실패 패턴을 살펴라

테스트 케이스가 실패하는 패턴으로 문제를 진단할 수 있다. 테스트 케이스를 최대한 꼼꼼히 짜야 하는 이유도 여기에 있다. 합리적인 순서로 정렬된 꼼꼼한 테스트 케이스는 실패 패턴을 드러낸다.

테스트 커버리지 패턴을 살펴라

통과하는 테스트가 실행하거나 실행하지 않는 코드를 살펴보면 실패하는 테스트 케이스의 실패 원인이 드러난다.

테스트는 빨라야 한다

느린 테스트 케이스는 실행하지 않게 된다. 일정이 촉박하면 느린 테스트 케이스를 제일 먼저 건너뛴다. 그러므로 테스트 케이스가 빨리 돌아가게 최대한 노력한다.

결론

이 장에서 소개한 휴리스틱과 냄새 목록은 완전하지 않으며, 그럴 수도 없다. 여기서 소개한 목록은 가치 체계를 피력할 뿐이다.
사실상 가치 체계는 이 책의 주제이자 목표다. 전문가 정신과 장인 정신은 가치에서 나온다. 그 가치에 기반을 둔 규율과 절제가 필요하다.

Footnotes

  1. 불충분한 시간이나 정보로 인하여 합리적인 판단을 할 수 없거나, 체계적이면서 합리적인 판단이 굳이 필요하지 않은 상황에서 사람들이 빠르게 사용할 수 있게 보다 용이하게 구성된 간편추론의 방법, 한국심리학회 (2020. 05). 《심리학용어사전》-heuristic 발견법(No3107) https://www.koreanpsychology.or.kr/psychology/glossary_view.asp?idx=3107

  2. https://en.wikipedia.org/wiki/Principle_of_least_astonishment

  3. https://en.wikipedia.org/wiki/Template_method_pattern

  4. https://en.wikipedia.org/wiki/Strategy_pattern

  5. 코드가 돌아간다는 사실을 아는 것과 돌아가기 위한 알고리즘이 올바르다는 사실을 아는 것은 다르다. 흔히 개발자들은 알고리즘이 올바르다고 확신하지 못한다. 게으른 까닭이라 하겠다.

  6. https://bbodeuk.github.io/Clean-Code/3%EC%9E%A5%20-%20%ED%95%A8%EC%88%98/#switch%EB%AC%B8

  7. https://bbodeuk.github.io/Clean-Code/6%EC%9E%A5%20-%20%EA%B0%9D%EC%B2%B4%EC%99%80%20%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0/#%EB%94%94%EB%AF%B8%ED%84%B0-%EB%B2%95%EC%B9%99